﻿using System;
using System.Management.Automation.Runspaces;
using System.Reflection;
using System.Threading;
using System.Linq;
using System.Collections.Generic;
using Citrix.Management.Automation;
using Citrix.XaAdminMobile.Util;
using Citrix.XenApp.Sdk;
using Citrix.XenApp.Commands;
using System.ComponentModel;
using Citrix.Cmp;
using System.Management.Automation;

namespace Citrix.XaAdminMobile.Model
{
    /// <summary>
    /// The model object that represents a single XenApp session.
    /// </summary>
    internal class SessionModel : ISessionModel
    {
        /// <summary>
        /// The XenApp SDK session object.
        /// </summary>
        private XASession session;

        /// <summary>
        /// The base model object.
        /// </summary>
        private readonly DefaultModel modelBase;

        /// <summary>
        /// The parent container that holds all session objects.
        /// </summary>
        private readonly SessionsModel sessionContainer;

        /// <summary>
        /// The unique key to identify this session.
        /// </summary>
        private readonly SessionKey key;

        /// <summary>
        /// Whether or not the full state has ever been loaded for this session.
        /// </summary>
        private bool isFullStateLoaded = false;

        /// <summary>
        /// The current action status - use to tell what action we are performing on the session
        /// if any.
        /// </summary>
        private SessionActionStatus sessionActionStatus = SessionActionStatus.None;

        /// <summary>
        /// Used to sync access to internal state.
        /// </summary>
        private object stateLock = new object();

        /// <summary>
        /// Set to true while we are (re) loading the state of this session.
        /// </summary>
        private bool isLoading = false;

        /// <summary>
        /// Holds the set of ISessionModel property names. We use this in
        /// <see cref="FindChangedProperties"/> when mapping changes in XASession and an
        /// instance of SessionModel. We use it to ignore properties on XASession that are not
        /// supported on ISessionModel.
        /// </summary>
        private static HashSet<string> SupportedProperties;

        /// <summary>
        /// Static ctor.
        /// </summary>
        static SessionModel()
        {
            SupportedProperties = new HashSet<string>();
            Type type = typeof(ISessionModel);
            var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
            foreach (var p in props)
            {
                SupportedProperties.Add(p.Name);
            }
        }

        /// <summary>
        /// Initialisation ctor.
        /// </summary>
        /// <param name="source">XenApp SDK PS source object.</param>
        /// <param name="modelRoot">The root model object.</param>
        /// <param name="parent">The sessions container we belong to.</param>
        /// <exception cref="ArgumentNullException"></exception>
        public SessionModel(XASession source, DefaultModel modelRoot, SessionsModel parent)
        {
            if (null == source)
            {
                throw new ArgumentNullException("source");
            }
            if (null == modelRoot)
            {
                throw new ArgumentNullException("modelRoot");
            }
            if (null == parent)
            {
                throw new ArgumentNullException("parent");
            }

            this.session = source;
            this.key = new SessionKey(source);
            this.sessionContainer = parent;
            this.modelBase = modelRoot;
        }

        /// <summary>
        /// Access session GUID
        /// </summary>
        public string AccessSessionGuid
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session.AccessSessionGuid;
                }
            }
        }

        /// <summary>
        /// User account name. Generally in the form: "domain" backslash "SAM account name".
        /// </summary>
        public string AccountName
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session.AccountName;
                }
            }
        }

        /// <summary>
        /// The state of the app launched in the session.
        /// </summary>
        public SessionApplicationState? ApplicationState
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session.ApplicationState;
                }
            }
        }

        /// <summary>
        /// The name of the published application associated with this session
        /// </summary>
        public string BrowserName
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session.BrowserName;
                }
            }
        }

        /// <summary>
        /// The client address.
        /// </summary>
        public string ClientAddress
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session.ClientAddress;
                }
            }
        }


        /// <summary>
        /// Client buffer size for HDX video.
        /// </summary>
        public string ClientBuffers
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session.ClientBuffers;
                }
            }
        }

        /// <summary>
        /// The build number of the client.
        /// </summary>
        public Int32? ClientBuildNumber
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session.ClientBuildNumber;
                }
            }
        }

        /// <summary>
        /// ClientCacheDisk
        /// </summary>
        public uint? ClientCacheDisk
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session.ClientCacheDisk;
                }
            }
        }

        /// <summary>
        /// ClientCacheLow
        /// </summary>
        public uint? ClientCacheLow
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session.ClientCacheLow;
                }
            }
        }

        /// <summary>
        /// ClientCacheMinBitmapSize
        /// </summary>
        public uint? ClientCacheMinBitmapSize
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session.ClientCacheMinBitmapSize;
                }
            }
        }

        /// <summary>
        /// ClientCacheSize
        /// </summary>
        public uint? ClientCacheSize
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session.ClientCacheSize;
                }
            }
        }

        /// <summary>
        /// ClientCacheTiny
        /// </summary>
        public uint? ClientCacheTiny
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session.ClientCacheTiny;
                }
            }
        }

        /// <summary>
        /// ClientCacheXms
        /// </summary>
        public uint? ClientCacheXms
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session.ClientCacheXms;
                }
            }
        }

        /// <summary>
        /// The client installation director.
        /// </summary>
        public string ClientDirectory
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session.ClientDirectory;
                }
            }
        }

        /// <summary>
        /// The client ID.
        /// </summary>
        public UInt32? ClientId
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session.ClientId;
                }
            }
        }

        /// <summary>
        /// The V4 IP address of the client.
        /// </summary>
        public string ClientIPV4
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session.ClientIPV4;
                }
            }
        }

        /// <summary>
        /// The name of the client device. For windows clients this is generally the Netbios
        /// name of the computer.
        /// </summary>
        public string ClientName
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session.ClientName;
                }
            }
        }

        /// <summary>
        /// An identifier based on client state like IP address, name, etc. This ID is unique
        /// most of the time.
        /// </summary>
        public Int32? ClientProductId
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session.ClientProductId;
                }
            }
        }

        /// <summary>
        /// The type of client running this session.
        /// </summary>
        public string ClientType
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session.ClientType;
                }
            }
        }

        /// <summary>
        /// The color depth of the client, or ColorDepth.Unknown if not applicable.
        /// </summary>
        public ColorDepth ColorDepth
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session.ColorDepth ?? ColorDepth.Unknown;
                }
            }
        }

        /// <summary>
        /// The last connection time of the session.
        /// </summary>
        public DateTime? ConnectTime
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session.ConnectTime;
                }
            }
        }

        /// <summary>
        /// The time of day that information about this session was retreived, in UTC. Null if
        /// not applicable.
        /// </summary>
        public DateTime? CurrentTime
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session.CurrentTime;
                }
            }
        }

        /// <summary>
        /// Whether direct X is enabled on the client.
        /// </summary>
        public bool? DirectXEnabled
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session.DirectXEnabled;
                }
            }
        }

        /// <summary>
        /// The last disconnect time of this session, in UTC. Null if not applicable.
        /// </summary>
        public DateTime? DisconnectTime
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session.DisconnectTime;
                }
            }
        }

        /// <summary>
        /// Encryption level of the session.
        /// </summary>
        public EncryptionLevel EncryptionLevel
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session.EncryptionLevel;
                }
            }
        }

        /// <summary>
        /// Whether flash is enabled on the client.
        /// </summary>
        public bool? FlashEnabled
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session.FlashEnabled;
                }
            }
        }

        /// <summary>
        /// The horizontal resolution of the session in pixels, or zero if not applicable.
        /// </summary>
        public int? HorizontalResolution
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session.HorizontalResolution;
                }
            }
        }

        /// <summary>
        /// Set to true when this model object is loading or reloading it's state from the server.
        /// </summary>
        public bool IsLoading
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.isLoading;
                }
            }
            private set
            {
                bool changed = false;
                lock (this.stateLock)
                {
                    if (this.isLoading != value)
                    {
                        this.isLoading = value;
                        changed = true;
                    }
                }
                if (changed)
                {
                    OnPropertyChanged("IsLoading");
                }
            }
        }

        /// <summary>
        /// True if the full state has ever been loaded for this session, false if only the
        /// summary state has been loaded. Once the full state has been loaded for a session
        /// this property will always return true, even if you do a refresh of just the summary
        /// state.
        /// </summary>
        public bool IsFullStateLoaded
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.isFullStateLoaded;
                }
            }
        }

        /// <summary>
        /// The last input time of this session, in UTC. Null if not applicable.
        /// </summary>
        public DateTime? LastInputTime
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session.LastInputTime;
                }
            }
        }

        /// <summary>
        /// The time that this session logged on, in UTC. Null if not applicable.
        /// </summary>
        public DateTime? LogOnTime
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session.LogOnTime;
                }
            }
        }

        /// <summary>
        /// Returns the sessions container object this session belongs to.
        /// </summary>
        public ISessionsModel Parent
        {
            get { return this.modelBase.Sessions; }
        }

        /// <summary>
        /// The protocol of this session.
        /// </summary>
        public SessionProtocolType? Protocol
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session.Protocol;
                }
            }
        }


        /// <summary>
        /// Refreshes the state of this session from the XenApp farm.
        /// </summary>
        /// <param name="loadFullState">True to load the full state of the session, false just
        /// to load the summary state.</param>
        /// <remarks>
        /// This call is non-blocking, the refresh will occur in a background thread.
        /// </remarks>
        public void Refresh(bool loadFullState)
        {
            ThreadPool.QueueUserWorkItem(wi =>
            {
                try
                {
                    this.IsLoading = true;
                    this.sessionContainer.IncrementSessionLoadCount();

                    XASession psSession = this.PSSession;
                    var detailsResult = this.modelBase.RunspacePool.ExecuteCommand(
                        new GetXASessionByObject(new XASession[] { psSession }) { ComputerName = this.modelBase.ConnectionTarget, Full = loadFullState });
                    if (null != detailsResult && detailsResult.Count() > 0)
                    {
                        XASession updatedSession = detailsResult.First();
                        if (null != updatedSession)
                        {
                            Update(updatedSession, loadFullState);
                        }
                    }
                }
                catch (InvalidRunspacePoolStateException ex)
                {
                    FTrace.Except(MethodBase.GetCurrentMethod(), ex);
                }
                catch (CmdletInvocationException ex)
                {
                    FTrace.Except(MethodBase.GetCurrentMethod(), ex);
                }
                finally
                {
                    this.IsLoading = false;
                    this.sessionContainer.DecrementSessionLoadCount();
                }
            });
        }


        /// <summary>
        /// The name of the XenApp server the session is running on.
        /// </summary>
        public string ServerName
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session.ServerName;
                }
            }
        }

        /// <summary>
        /// The server side buffer size for HDX video.
        /// </summary>
        public string ServerBuffers
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session.ServerBuffers;
                }
            }
        }

        /// <summary>
        /// The Windows session ID for this session.
        /// </summary>
        public int? SessionId
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session.SessionId;
                }
            }
        }

        /// <summary>
        /// Used to track when an action is being executed on a session, and the type of the
        /// action being executed. This is used to present UI feedback for a session.
        /// </summary>
        public SessionActionStatus SessionActionStatus
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.sessionActionStatus;
                }
            }
            set
            {
                bool changed = false;
                lock (this.stateLock)
                {
                    if (this.sessionActionStatus != value)
                    {
                        changed = true;
                        this.sessionActionStatus = value;
                    }
                }
                if (changed)
                {
                    OnPropertyChanged("SessionActionStatus");
                }
            }
        }

        /// <summary>
        /// The session name.
        /// </summary>
        public string SessionName
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session.SessionName;
                }
            }
        }

        /// <summary>
        /// The current session state.
        /// </summary>
        public SessionConnectionState State
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session.State ?? SessionConnectionState.Unknown;
                }
            }
        }

        /// <summary>
        /// Whether or not USB is enabled on the client.
        /// </summary>
        public bool? UsbEnabled
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session.UsbEnabled;
                }
            }
        }

        /// <summary>
        /// The vertical resolution of the session in pixels, or zero if not applicable.
        /// </summary>
        public int? VerticalResolution
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session.VerticalResolution;
                }
            }
        }

        /// <summary>
        /// The Virtual IP address of the session, or empty if Virtual IP is not in use.
        /// </summary>
        public string VirtualIP
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session.VirtualIP;
                }
            }
        }

        /// <summary>
        /// Whether Windows Media Player is enabled on the client.
        /// </summary>
        public bool? WmpEnabled
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session.WmpEnabled;
                }
            }
        }

        /// <summary>
        /// A key to uniquely identify this session.
        /// </summary>
        internal SessionKey Key
        {
            get { return this.key; }
        }

        /// <summary>
        /// The underlying XA SDK PowerShell session object. Used when we make SDK calls.
        /// </summary>
        internal XASession PSSession
        {
            get
            {
                lock (this.stateLock)
                {
                    return this.session;
                }
            }
        }

        /// <summary>
        /// Updates the model state to reflect the newly queried state from the XenApp SDK.
        /// </summary>
        /// <param name="source">New XA SDK session object.</param>
        /// <param name="fullState">True if the supplied session object contains the full state.</param>
        internal void Update(XASession source, bool fullState = false)
        {
            if (null != source)
            {
                XASession old;
                HashSet<string> properties = new HashSet<string>();
                lock (this.stateLock)
                {
                    old = this.session;
                    this.session = source;
                    if (!this.isFullStateLoaded && fullState)
                    {
                        this.isFullStateLoaded = fullState;
                        properties.Add("IsFullStateLoaded");
                    }
                }

                FindChangedProperties(properties, old, source);
                foreach (var p in properties)
                {
                    OnPropertyChanged(p);
                }
            }
        }

        /// <summary>
        /// This method inspects an existing XASession instance with a new one and figures out
        /// which properties have changed. We use this to figure out the names of the affected
        /// properties on a SessionModel instance that will have changed. This relies on the
        /// property names on XASession being the same as those defined on ISessionModel.
        /// </summary>
        /// <param name="changedPropertyNames">The output set of changed property names.</param>
        /// <param name="old">The new XASession instance, must not be null.</param>
        /// <param name="source">The old XASession instance, must not be null.</param>
        private static void FindChangedProperties(HashSet<string> changedPropertyNames, XASession old, XASession source)
        {
            Type type = source.GetType();

            var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
            foreach (var f in props)
            {
                object oldVal = GetPropValue(old, f.Name);
                object newVal = GetPropValue(source, f.Name);
                Type propType = f.PropertyType;

                if (SupportedProperties.Contains(f.Name))
                {
                    if (typeof(string) == propType)
                    {
                        if (!StringCompare(newVal, oldVal))
                        {
                            changedPropertyNames.Add(f.Name);
                        }
                    }
                    else if (typeof(string[]) == propType)
                    {
                        if (!StringArrayCompare(oldVal, newVal))
                        {
                            changedPropertyNames.Add(f.Name);
                        }
                    }
                    else
                    {
                        if (!DefaultCompare(oldVal, newVal))
                        {
                            changedPropertyNames.Add(f.Name);
                        }
                    }
                }
                else
                {
                    FTrace.Debug(MethodBase.GetCurrentMethod(), "Ignoring: {0}", f.Name);
                }
            }
        }

        /// <summary>
        /// Given an object and a property name on the object, this method retrieves the value
        /// of that property.
        /// </summary>
        /// <param name="src">Target object, must not be null.</param>
        /// <param name="propName">The name of the property, must be a valid property name.</param>
        /// <returns>The value.</returns>
        private static object GetPropValue(object src, string propName)
        {
            return src.GetType().GetProperty(propName).GetValue(src, null);
        }

        /// <summary>
        /// Performs a case insensitive comparison of two strings.
        /// </summary>
        /// <param name="lhs">String to compare, maybe null.</param>
        /// <param name="rhs">String to compare, maybe null.</param>
        /// <returns>True if equal, false if not equal.</returns>
        private static bool StringCompare(object lhs, object rhs)
        {
            return string.Equals((string)lhs, (string)rhs, StringComparison.OrdinalIgnoreCase);
        }

        /// <summary>
        /// Performs a comparison of two string arrays.
        /// </summary>
        /// <param name="lhs">Array to compare, maybe null.</param>
        /// <param name="rhs">Array to compare, maybe null.</param>
        /// <returns>True if equal, false if not equal.</returns>
        private static bool StringArrayCompare(object lhs, object rhs)
        {
            if (null != lhs && null != rhs)
            {
                string[] left = (string[])lhs;
                string[] right = (string[])rhs;

                if (left.Length != right.Length)
                {
                    return false;
                }
                for (int i = 0; i < left.Length; ++i)
                {
                    if (!string.Equals(left[i], right[i]))
                    {
                        return false;
                    }
                }
                return true;
            }
            else if (null == lhs && null == rhs)
            {
                return true;
            }
            else
            {
                return false;
            }
        }

        /// <summary>
        /// Performs a default comparison of two objects, using the object.Equals override.
        /// </summary>
        /// <param name="lhs">Object to compare, maybe null.</param>
        /// <param name="rhs">Object to compare, maybe null.</param>
        /// <returns>True if equal, false if not equal.</returns>
        private static bool DefaultCompare(object lhs, object rhs)
        {
            if (null != lhs && null != rhs)
            {
                return lhs.Equals(rhs);
            }
            else if (null == lhs && null == rhs)
            {
                return true;
            }
            else
            {
                return false;
            }
        }

        #region INotifyPropertyChanged Members

        /// <summary>
        /// Fired whenever a public property changes.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Fires the property change event. Maybe called from any thread - we always fire the
        /// event on the main UI thread.
        /// </summary>
        /// <param name="propertyName">The name of the property that changed.</param>
        private void OnPropertyChanged(string propertyName)
        {
            NotifyHelper.VerifyProperty(propertyName, this);
            if (this.PropertyChanged != null)
            {
                this.PropertyChanged.Raise(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        #endregion // INotifyPropertyChanged Members
    }
}
